#!/bin/bash

. $LKP_SRC/lib/kexec.sh
. $LKP_SRC/lib/http.sh
. $LKP_SRC/lib/qemu.sh
. $LKP_SRC/lib/unit.sh
. $LKP_SRC/lib/job-init.sh
. $LKP_SRC/lib/lkp_cmd.sh
. $LKP_SRC/lib/lkp_path.sh

script_name=$(basename $0)

[ -n "$SUDO_USER" ] && HOME=$(eval "echo ~$SUDO_USER")

usage()
{
	cat <<EOF
Usage: lkp $script_name [-o RESULT_ROOT] [-p VDISK_PATH] [-s SSH_PORT] [-k bzImage] [-m modules.cgz] [-e] job.sh

options:
	-o  RESULT_ROOT		 	dir for storing all results
	-s  SSH_PORT		 	forward ssh port to host
	-p  VDISK_PATH			specify vdisk path
	-k  bzImage			specify bzImage as kernel image
	-m  modules.cgz			specify kernel modules
	-e				use external network (download from 01.org)

Note:
This script uses qemu to start a VM to run LKP test-job.
It downloads kernel, initrd, bm_initrd, modules_initrd through LKP_SERVER,
and  generates lkp-initrd locally and creates job-initrd with 'job.sh'  you specified.

You can check test results in dir '/tmp/vm_test_result/' or a RESULT_ROOT you specified.
EOF
	exit 1
}

get_job_env()
{
	[[ $1 ]] || return
	[[ $2 ]] || return
	job_script=$(realpath $1) env=$2 bash -c 'source $job_script && export_top_env > /dev/null 2>&1 && eval echo \$$env'
}

create_lkp_home()
{
	[[ -e $HOME/.lkp ]] && return

	cat <<-EOF
	The approx. disk space requirements are

	10M             simple boot test in rootfs openwrt
	50M		simple boot test in rootfs debian
	1G              plan to run a number of different tests
	100G or more    IO tests

	Please enter a dir with enough disk space, or simply press Enter to accept the default.
	You may still symlink $HOME/.lkp to a more suitable place in future.
	EOF

	local dir
	read -p "$HOME/.lkp => " dir

	[[ $dir ]] && {
	dir=$(realpath $dir)
	mkdir -p $dir || exit
	ln -sT $dir $HOME/.lkp || exit
	}

	mkdir -p $HOME/.lkp/cache
	mkdir -p $HOME/.lkp/result
	mkdir -p $HOME/.lkp/qemu-img
}

replace_script_partition_val()
{
	local disk_names=(/dev/vd{a..z})
	local job_script="$1"
	local nr_hdd_vdisk="$(echo $hdd_partitions | wc -w)"
	local nr_ssd_vdisk="$(echo $ssd_partitions | wc -w)"
	local nr_swap_vdisk="$(echo $swap_partitions | wc -w)"
	local nr_rootfs_vdisk="$(echo $rootfs_partition | wc -w)"

	[[ $nr_hdd_partitions ]] || nr_hdd_partitions=$nr_hdd_vdisk
	[[ $nr_ssd_partitions ]] || nr_ssd_partitions=$nr_ssd_vdisk

	VDISK_NUM=$((nr_hdd_partitions+nr_ssd_partitions+nr_swap_vdisk+nr_rootfs_vdisk))

	[[ "$hdd_partitions$ssd_partitions$swap_partitions$rootfs_partition" =~ '/dev/vda' ]] && return

	if ((VDISK_NUM)); then
		local index=0
		local vdisk_hdd_val="${disk_names[@]:$index:$nr_hdd_partitions}"
		index=$((index+nr_hdd_partitions))
		local vdisk_ssd_val="${disk_names[@]:$index:$nr_ssd_partitions}"
		index=$((index+nr_ssd_partitions))
		local vdisk_rootfs_val="${disk_names[@]:$index:$nr_rootfs_vdisk}"
		index=$((index+nr_rootfs_vdisk))
		local vdisk_swap_val="${disk_names[@]:$index:$nr_swap_vdisk}"

		# First remove lines between two export, or it will be left
		# export ssd_partitions='/dev/disk/by-id/nvme-INTEL_SSDPE2KX040T7_PHLF7414019F4P0IGN-part2
		# /dev/disk/by-id/nvme-INTEL_SSDPE2KX040T7_PHLF7414019F4P0IGN-part3'
		# export swap_partitions=
		sed -i  -e '/export hdd_partitions/,/export/{//!d}' \
			-e '/export ssd_partitions/,/export/{//!d}' \
			-e '/export swap_partitions/,/export/{//!d}' \
			-e '/export rootfs_partitions/,/export/{//!d}' \
			$job_script
		sed -i	-e "s%export hdd_partitions=.*%export hdd_partitions='${vdisk_hdd_val}'%" \
			-e "s%export ssd_partitions=.*%export ssd_partitions='${vdisk_ssd_val}'%" \
			-e "s%export swap_partitions=.*%export swap_partitions='${vdisk_swap_val}'%" \
			-e "s%export rootfs_partition=.*%export rootfs_partition='${vdisk_rootfs_val}'%" \
			$job_script
	fi
}

create_job_initrd()
{
	local job_sh=$CACHE_DIR/${job_file%.yaml}.sh
	[[ -d $CACHE_DIR/$job_initrd_dir ]] && {
		rm -rf "${CACHE_DIR:?}/$job_initrd_dir" || return
	}
	mkdir -p $CACHE_DIR/$job_initrd_dir
	cp $job_script $job_sh
	chmod +x $job_sh
	local archive=$CACHE_DIR/job
	(
		cd $CACHE_DIR || exit
		{
			local dir=
			for d in $(echo $job_initrd_dir | tr '/' ' ')
			do
				dir="$dir""$d"/
				echo $dir
			done
			find ${job_initrd_dir#/}/*
		} | cpio -o -H newc -F $archive.cpio
		gzip -n -9 $archive.cpio
		mv -f $archive.cpio.gz $job_initrd
	)
}

get_qemu_kernel_initrd()
{
	local lkp_initrd
	local job_initrd
	local final_initrd=$CACHE_DIR/final_initrd
	[[ $opt_kernel_image ]] || download_kernel
	lkp_initrd='' job_initrd='' download_initrd
	local user_lkp_initrd=$1
	local user_job_initrd=$2
	cat "$concatenate_initrd" "$user_lkp_initrd" "$user_job_initrd" > "$final_initrd"

	initrd_option="-initrd $final_initrd"
}

# limit $1 to MemAvailable/2
max_sane_qemu_memory()
{
	local mem_kb="$(to_kb $1)"

	export_meminfo

	[[ $MemAvailable ]] ||
	(( MemAvailable = MemFree + (Active_file/2) + Inactive_file ))

	(( mem_kb > MemAvailable / 2 )) && mem_kb=$((MemAvailable / 2))

	echo $((mem_kb >> 10))M
}

setup_vdisk_root()
{
	vm_name=$testbox
	
	if [[ "$opt_vdiskpath" ]]; then
		[[ -d "$opt_vdiskpath" ]] || {
			echo "$opt_vdiskpath: no such directory"
			exit 1
		}
		VDISK_ROOT="$opt_vdiskpath"
	else
		VDISK_ROOT=/tmp/vdisk-$USER
		[[ -d $VDISK_ROOT ]] || mkdir -p $VDISK_ROOT
	fi
}

while getopts "o:p:s:k:m:e" opt
do
	case $opt in
		o ) opt_result_root="$OPTARG" ;;
		s ) opt_ssh="$OPTARG" ;;
		p ) opt_vdiskpath="$OPTARG" ;;
		k ) opt_kernel_image="$OPTARG" ;;
		m ) opt_modules="$OPTARG" ;;
		e ) HTTP_PREFIX=https://download.01.org/0day-ci/lkp-qemu ;;
		? ) usage ;;
	esac
done

shift $((OPTIND-1))

unset DISPLAY

job_script=$1

[ -n "$job_script" ] || usage

sed -i 's/\r//' $job_script
sed -i 's/LKP_SERVER=.*$/LKP_LOCAL_RUN=1/g' $job_script

create_lkp_user
create_lkp_home
export CACHE_DIR=$HOME/.lkp/cache
mkdir -p $CACHE_DIR

. $job_script export_top_env
replace_script_partition_val $job_script
[[ $job_file ]] || job_file=${job_script%.sh}.yaml

if [[ $opt_modules ]]; then
	modules_initrd=$opt_modules
	ln -sf "$(realpath $opt_modules)" $CACHE_DIR/$(basename $opt_modules)
else
	unset modules_initrd # modules_initrd has existed in job.sh, need to flush it
fi

create_lkp_src_initrd()
{
	if [[ "$kconfig" =~ ^(i386|x86_64)- ]]; then
		local arch=${kconfig%%-*}
	else
		local arch=$(arch)
	fi

	if [ -d $LKP_SRC/.git ]; then
		local head_commit=$(cd $LKP_SRC && git rev-list -n1 HEAD)
		local diff_id=$(cd $LKP_SRC && git diff | git patch-id | cut -f1 -d' ')
		local src_sig=${head_commit:0:12}_${diff_id:0:12}
	else
		local src_sig=$(ls -lR $LKP_SRC|md5sum|cut -c1-12)
	fi
	lkp_initrd=$CACHE_DIR/lkp-$arch-$src_sig.cgz
	[[ -f $lkp_initrd ]] || {
		archive="${CACHE_DIR}"

		# shellcheck disable=SC2164
		pushd "$LKP_SRC/pkg/lkp-src"
		LKP_USER="$user" PACMAN=true BUILDDIR=/tmp/$USER CARCH=$arch PKGEXT=.cgz CGZDEST="$CACHE_DIR/lkp-$arch.cgz" \
		$LKP_SRC/sbin/makepkg --config $(lkp_src)/etc/makepkg.conf -e
		[[ -f $CACHE_DIR/lkp-$arch.cgz ]] || {
			echo "Failed to pack: $CACHE_DIR/lkp-$arch.cgz"
			exit 1
		}
		mv $CACHE_DIR/lkp-$arch.cgz $lkp_initrd
		# shellcheck disable=SC2164
		popd
	}
}
create_lkp_src_initrd

# create job_initrd.cgz
job_sig=$(md5sum $job_script | cut -c1-5)
job_initrd=$CACHE_DIR/${job_file%.yaml}-$job_sig.cgz
job_initrd_dir=${job_file%/*}
[[ -f $job_initrd ]] || create_job_initrd

# if job.sh not include bootloader_append entry, add default content
if [ -n "$bootloader_append" ]; then
	bootloader_append=$(echo "$bootloader_append" | tr '\n' ' ' | sed -e 's/ttyS[1-9],/ttyS0,/g')
else
	bootloader_append="root=/dev/ram0 job=$job_file user=$user  ARCH=x86_64 kconfig=x86_64-rhel commit=051d101ddcd268a7429d6892c089c1c0858df20b branch=linux-devel/devel-hourly-2015033109 max_uptime=1247 RESULT_ROOT=$result_root earlyprintk=ttyS0,115200 rd.udev.log-priority=err systemd.log_target=journal systemd.log_level=warning debug apic=debug sysrq_always_enabled rcupdate.rcu_cpu_stall_timeout=100 panic=-1 softlockup_panic=1 nmi_watchdog=panic oops=panic load_ramdisk=2 prompt_ramdisk=0 console=ttyS0,115200 console=tty0 vga=normal rw"
fi

# create vm result path
if [ -z $opt_result_root ]; then
	# create real result_root layout similar to the one in /result
	_result_root=$HOME/.lkp/$(dirname $result_root)
	for i in {0..1000}
	do
		vm_result_path=$_result_root/$i
		[[ -d $vm_result_path ]] || break
	done

	if [ $i -eq 1000 ]; then
		echo "!!!Exceed maximum result root number (1000), please delete some old results"
		exit 1
	fi
	echo "result_root: $vm_result_path"
else
	vm_result_path=$opt_result_root
fi
mkdir -p $vm_result_path

if [[ $HTTP_PREFIX ]]; then
	: # use environment value
elif [[ $HOSTNAME = inn ]]; then
	LKP_SERVER=inn
elif grep -q intel.com /etc/resolv.conf; then
	LKP_SERVER=0day.sh.intel.com
else
	LKP_SERVER=
	HTTP_PREFIX=https://download.01.org/0day-ci/lkp-qemu
fi

LKP_USER="lkp"
if [ -z $QEMU_MODEL ]; then
	if get_job_env $job_script 'model' | grep -q 'qemu-system-i386'; then
		QEMU_MODEL='qemu-system-i386'
	else
		QEMU_MODEL='qemu-system-x86_64'
	fi
fi

run_kvm()
{
	trap - EXIT

	local job_script=$1
	local mem_mb="$(max_sane_qemu_memory $memory)"
	local mount_tag=9p/virtfs_mount
	local need_mem
	need_mem="$(get_job_env $job_script 'need_memory')" || return
	[[ "$need_mem" != "" ]] && {
		local need_mem_mb="$(to_mb $need_mem)"
		local testsuite
		testsuite="$(get_job_env $job_script 'suite')" || return
		[[ "$need_mem_mb" -gt "${mem_mb: : -1}" ]] && echo "Warning: test suite $testsuite needs memory: ${need_mem_mb}Mb, actual memory: ${mem_mb: : -1}Mb"
	}
	netdev_option="-device e1000,netdev=net0 "
	netdev_option+="-netdev user,id=net0"
	KVM_COMMAND=(
		$QEMU_MODEL -enable-kvm
		-fsdev local,id=test_dev,path=$vm_result_path,security_model=none -device virtio-9p-pci,fsdev=test_dev,mount_tag=$mount_tag
		-kernel ${opt_kernel_image:-$kernel_file}
		-append "$bootloader_append ip=dhcp result_service=$mount_tag"
		$initrd_option
		-smp $nr_cpu
		-m $mem_mb
		-no-reboot
		-watchdog i6300esb
		-rtc base=localtime
		$qemu_netdev_option
		$qemu_console_option
		$QEMU_DRIVE_OPTION
	)
	echo "exec command: ${KVM_COMMAND[@]}"
	"${KVM_COMMAND[@]}"
}

# lkp qemu alway log guest to stdout
setup_qemu_console()
{
	qemu_console_option="-display none -monitor null -serial stdio"
}

get_qemu_kernel_initrd $lkp_initrd $job_initrd
setup_qemu_console
setup_qemu_netdev
setup_vdisk_root
setup_qemu_drives
run_kvm $job_script
cleanup_qemu_drives
